/*******************************************************************************************
 * Copyright (c) 2006-2009 Alex Robson, Rob Simmons, Craig Israel, and Rachel Twyford
 *
 * Use of this software in any form requires that you have both 
 * read and agreed to the following terms:
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions: 
 *
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the 
 * documentation and/or other materials provided with the distribution.
 * Neither the name of Nvigorate nor the names of its contributors may 
 * be used to endorse or promote products derived from this software without 
 * specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ****************************************************************************************/

using System;
using System.Collections.Generic;
using System.Linq;
using Nvigorate.Common;
using Nvigorate.Common.Reflection;
using Nvigorate.Data.Query;
using Nvigorate.Extensions;
using Nvigorate.Relational.Mapping;
using Nvigorate.Relational.Mapping.Index;
using Nvigorate.Relational.Mapping.Interface;
using Nvigorate.Relational.Repository;

namespace Nvigorate.Relational.Builders
{
    public class SelectBuilder
    {
        private IRepositoryMapIndex _repositoryMapIndex;
        private ILoadInstruction _typeInstruction;
        private IBindingMap _map;
        private IBindingMap _parentMap;
        private IEnumerable<IAssignment> _assignments;
        private TableVariable _holdingTable;
        private Insert _insertIntoHolding;
        private Select _selectIntoHolding;
        private Select _selectFromHolding;
        private ITargetRelationship _owningRelationship;
        private Table[] _joinTables;
        private string _tableSuffix = "";
        private string _parentSuffix = "";
        private string _rootName = "";
        private Dictionary<string, string> _joinAlias = new Dictionary<string, string>();

        protected string GetOwnedBy(IAssignment assignment)
        {
            return assignment.Source.Entity + _tableSuffix;
        }

        protected string GetOwnedBy()
        {
            return Map.TargetType.Name + _tableSuffix;
        }

        #region Implementation of IQueryBuildingInfo

        public string Source { get; set; }
        public IMapIndex MapIndex { get; set; }
        public ILoadInstruction Instruction { get; set; }

        #endregion

        #region Metadata

        private IRepositoryMapIndex RepositoryMapIndex
        {
            get
            {
                _repositoryMapIndex = _repositoryMapIndex ?? new RepositoryMapIndex(MapIndex, Source);
                return _repositoryMapIndex;
            }
        }

        private Type TargetType
        { get; set; }

        private ILoadInstruction TypeInstruction
        {
            get
            {
                _typeInstruction = _typeInstruction ?? Instruction.GetInstructionForType(TargetType) ?? Instruction.GetDefaultInstruction(TargetType);
                return _typeInstruction;
            }
        }

        private IBindingMap Map
        { 
            get
            {
                _map = _map ?? RepositoryMapIndex.GetMap(TargetType);
                return _map;
            }
        }

        private IBindingMap ParentMap
        {
            get
            {
                _parentMap = _parentMap ?? RepositoryMapIndex.GetMap(_owningRelationship.SubjectType);
                return _parentMap;
            }
        }

        private IEnumerable<IAssignment> Assignments
        {
            get
            {
                if(_assignments == null)
                {
                    var assignmentFilter = new Func<IAssignment, bool>(
                        a => 
                        (TypeInstruction.Properties.Count == 0 ||
                         TypeInstruction.Properties.Contains(a.Target.Field) ||
                         a.PrimaryKey) 
                        //&& !a.Target.Field.Contains(".")
                        );

                    _assignments = Map
                        .Sources.Values.Where(s => s.CanSelect)
                        .SelectMany(s => s.Assignments)
                        .Where(assignmentFilter)
                        .Concat(AdditionalAssignments);
                }
                return _assignments;
            }
        }

        private string Root
        {
            get
            {
                _rootName = string.IsNullOrEmpty(_rootName) ? Map.GetRootName() : _rootName;
                return _rootName;
            }
        }

        private Table[] JoinTables
        {
            get
            {
                _joinTables = _joinTables ?? BuildJoinTables(null);
                if (_owningRelationship != null)
                {
                    var tables = new[] {ParentJoin};
                    _joinTables = _joinTables == null ? tables :_joinTables.Concat(tables).ToArray();
                }
                return _joinTables;
            }
        }
        
        private Select SelectIntoHoldingTable
        {
            get
            {
                if(_selectIntoHolding == null)
                {
                    _selectIntoHolding = Select.Query(
                        Table.From(Root)
                            .Join(JoinTables),
                        Assignments.Select
                                    (a =>
                                     Select.Column(a.Source.Field)
                                         .OwnedBy(a.Source.Entity)
                                         .AliasOwner(a.Source.Entity == Root ? Root : _joinAlias[a.Source.Entity])
                                         .As(string.Format("{0}.{1}", GetOwnedBy(a), a.Source.Field)))
                            .ToQueryList()
                        );

                    if (TypeInstruction.PageIndex > 0 && TypeInstruction.PageSize > 0)
                        _selectIntoHolding.Qualifiers.Add(Page.PageSize(TypeInstruction.PageSize).PageIndex(TypeInstruction.PageIndex));

                    if (TypeInstruction.Criteria != null && TypeInstruction.Criteria.Count > 0)
                        _selectIntoHolding.Qualifiers.Add(new Where(TranslateCriteria(TypeInstruction.Criteria)));

                    if (TypeInstruction.OrderClause != null && TypeInstruction.OrderClause.ColumnList.Count > 0)
                        _selectIntoHolding.Qualifiers.Add(TypeInstruction.OrderClause);
                }

                return _selectIntoHolding;
            }
        }

        #endregion

        public Insert InsertIntoHoldingTable
        {
            get
            {
                _insertIntoHolding = _insertIntoHolding ??
                                     Insert.Query(
                                         HoldingTable.Table,
                                         Assignments.Select(a =>
                                                            Insert.Column(a.Source.Field)
                                                                .OwnedBy(GetOwnedBy(a))
                                             ).ToQueryList(),
                                         SelectIntoHoldingTable
                                         );
                return _insertIntoHolding;
            }
        }

        public Select SelectFromHoldingTable
        {
            get
            {
                _selectFromHolding = _selectFromHolding ??
                                     Select.Query(HoldingTable.Table, 
                                        Assignments.Select(a => Column.Named(string.Format("{0}.{1}", GetOwnedBy(a), a.Source.Field)).As(a.Target.Field))
                                        .ToQueryList());
                return _selectFromHolding;
            }
        }

        public TableVariable HoldingTable
        {
            get
            {
                _holdingTable = _holdingTable ??
                                new TableVariable(
                                    Table.Variable(GetOwnedBy()),
                                    Assignments.Select
                                        (a =>
                                         TableVariable.Column(a.Source.Field)
                                             .OwnedBy(GetOwnedBy(a))
                                             .Type(Reflector.GetMemberType(TargetType, a.Target.Field))
                                        ).ToQueryList()
                                    );
                return _holdingTable;
            }
        }

        protected Table[] BuildJoinTables(ISourceMap root)
        {
            if (root == null)
                root = Map.GetRoot();

            var joined = Map.GetChildSources(root.SourceName);
            var additionalJoins = AdditionalJoins;

            if (joined.Count() > 0)
            {
                return joined
                    .Select(joinTable =>
                        {
                            var joinAlias = string.Format("JOIN{0}", _joinAlias.Count);
                            _joinAlias.Add(joinTable.Join.Relative, joinAlias);
                            var joinInstruction = Table.From(joinTable.Join.Relative)
                                .As(joinAlias)
                                .On(TranslateCriteria(joinTable.Join.JoinCriteria))
                                .JoinInstruction(JoinType.Left);

                            if(root.SourceName != joinTable.SourceName)
                                joinInstruction.Join(BuildJoinTables(joinTable));

                            return joinInstruction;
                                                 
                        })
                    .Concat(
                    additionalJoins
                    )
                    .ToArray();
            }
            else if (additionalJoins.Count() > 0)
            {
                return additionalJoins.ToArray();
            }
            else
            {
                return null;
            }
        }

        protected IEnumerable<IAssignment> AdditionalAssignments
        {
            get
            {
                if (Map.Relationships != null)
                {
                    var manyToManyJoins =
                        Map
                            .Relationships
                            .OfType<ManyToManyRelationship>()
                            .Select(r =>
                                    r.Container.RelativePairs.Select(p =>
                                     new MemberAssignment()
                                         {
                                             Source = new AssignmentPoint(r.Container.ContainerName, p.Source),
                                             Target = new AssignmentPoint(r.RelativeType.AssemblyQualifiedName, p.Target)
                                         })
                            );

                    return manyToManyJoins.Flatten().Cast<IAssignment>();
                }
                return new List<IAssignment>().Select(a => a);
            }
        }

        protected IEnumerable<Table> AdditionalJoins
        {
            get
            {
                var root = Map.GetRoot();
                var joins = Instruction.AdditionalJoins
                    .Where(j => j.Key == root.SourceName)
                    .Select(j => j.Value);

                joins.ForEach(j =>
                                  {
                                      if(!_joinAlias.ContainsKey(j.Name))
                                      {
                                          var joinAlias = string.Format("JOIN{0}", _joinAlias.Count);
                                          _joinAlias.Add(j.Name, joinAlias);
                                          j.As(joinAlias);
                                          j.JoinCriteria.SelectMany(c => c.Criteria).ForEach(c =>
                                                                     {
                                                                         var equal = c as Equal;
                                                                         var column1 = equal.Field as Column;
                                                                         var column2 = equal.To as Column;
                                                                         if(column1 != null && column1.Owner.Equals(j.Name))
                                                                         {
                                                                             column1.AliasOwner(joinAlias);    
                                                                         }
                                                                         else if(column2 != null && column2.Owner.Equals(j.Name))
                                                                         {
                                                                             column2.AliasOwner(joinAlias);
                                                                         }
                                                                     });
                                      }
                                  });

                if (Map.Relationships != null)
                {
                    var manyToManyJoins =
                        Map
                            .Relationships
                            .OfType<ManyToManyRelationship>()
                            .Where(r => !Instruction.AdditionalJoins.Values.Select(j => j.Name).Contains(r.Container.ContainerName))
                            .Select(r => 
                                {
                                    if (!_joinAlias.ContainsKey(r.Container.ContainerName))
                                    {
                                        var joinAlias = string.Format("JOIN{0}", _joinAlias.Count);
                                        _joinAlias.Add(r.Container.ContainerName, joinAlias);
                                    }
                                    var onClause = new CriteriaClause(r.Container.SubjectPairs.Select(p =>
                                        {
                                            var assignment = Map.GetAssignmentsByTargetField(p.Target).First();
                                            return Criterion.Column(p.Source, _joinAlias[r.Container.ContainerName])
                                                       .EqualTo(Column.Named(assignment.Source.Field).OwnedBy(assignment.Source.Entity)) as IClause;
                                        }));

                                    return Table.LeftJoin(r.Container.ContainerName)
                                        .As(_joinAlias[r.Container.ContainerName])
                                        .On(new CriteriaClause(onClause));
                                }
                            );

                    joins = joins.Concat(manyToManyJoins);
                }
                return joins;
            }
        }

        public SearchCriteria TranslateCriteria(SearchCriteria criteria)
        {
            criteria.ForEach(TranslateClause);
            return criteria;
        }

        public void TranslateClause(CriteriaClause clause)
        {
            clause.Criteria.OfType<CriteriaClause>().ForEach(TranslateClause);
            clause.Criteria.OfType<Criterion>().ForEach(TranslateCriterion);
        }

        public void TranslateCriterion(Criterion criterion)
        {
            if(criterion.Field is Property)
            {
                var property = criterion.Field as Property;
                var assignment = _repositoryMapIndex.GetMap(property.Type ?? Instruction.LoadType)
                                          .GetAssignmentsByPredicate(a => a.Target.Field == property)
                                          .FirstOrDefault();
                if (assignment != null && _joinAlias.ContainsKey(assignment.Source.Entity))
                {
                    criterion.Field = Column.Named(assignment.Source.Field)
                        .OwnedBy(assignment.Source.Entity)
                        .AliasOwner(_joinAlias[assignment.Source.Entity]);
                }
            }
            else
            {
                var column = criterion.Field as Column;
                if(_joinAlias.ContainsKey(column.Owner))
                {
                    column.AliasOwner(_joinAlias[column.Owner]);
                }
            }

            if(criterion is Equal)
            {
                var equal = criterion as Equal;
                if(equal.To is Property)
                {
                    var property = equal.To as Property;
                    var assignment = _repositoryMapIndex.GetMap(property.Type)
                                              .GetAssignmentsByPredicate(a => a.Target.Field == property)
                                              .FirstOrDefault();
                    if (assignment != null && _joinAlias.ContainsKey(assignment.Source.Entity))
                    {
                        equal.To = Column.Named(assignment.Source.Field).AliasOwner(_joinAlias[assignment.Source.Entity]);
                    }
                }
                else if(equal.To is Column)
                {
                    var column = equal.To as Column;
                    if (_joinAlias.ContainsKey(column.Owner))
                    {
                        column.AliasOwner(_joinAlias[column.Owner]);
                    }
                }
            }
        }

        public Table ParentJoin
        {
            get
            {
                var parentTable = _owningRelationship.SubjectType.Name + _parentSuffix;
                var owningOneToMany = _owningRelationship as OneToManyRelationship;
                var owningManyToMany = _owningRelationship as ManyToManyRelationship;
                //_joinAlias.Add(parentTable, joinAlias);

                var joinAlias = "OWNER";
                return Table
                    .Variable(parentTable)
                    .JoinInstruction(JoinType.Inner)
                    .As(joinAlias)
                    .On(owningOneToMany != null ? OneToManyOwnerClause(owningOneToMany, joinAlias) : ManyToManyOwnerClause(owningManyToMany, joinAlias));
            }
        }

        public CriteriaClause ManyToManyOwnerClause(ManyToManyRelationship owningManyToMany, string parentTable)
        {
            var keys = owningManyToMany.Container.SubjectPairs.ZipToDictionary(owningManyToMany.Container.RelativePairs);

            return new CriteriaClause(
                keys
                    .Select(c =>
                    {
                        var parentAssignment = ParentMap.GetAssignmentsByTargetField(c.Key.Target).First();
                        var childAssignment = Map.GetAssignmentsByTargetField(c.Value.Target).First();
                        return
                            Criterion.Column(
                                string.Format("{0}.{1}", parentAssignment.Source.Entity,
                                              parentAssignment.Source.Field), parentTable)
                                .EqualTo(
                                Column.Named(childAssignment.Source.Field).OwnedBy(
                                    childAssignment.Source.Entity))
                            as IClause;
                    }
                    )
                );
        }

        public CriteriaClause OneToManyOwnerClause(OneToManyRelationship owningOneToMany, string parentTable)
        {
            return new CriteriaClause(
                owningOneToMany.Constraints
                    .Select(c =>
                                {
                                    var parentAssignment = ParentMap.GetAssignmentsByTargetField(c.Key).First();
                                    var childAssignment = Map.GetAssignmentsByTargetField(c.Value).First();
                                    return
                                        Criterion.Column(
                                            string.Format("{0}.{1}", parentAssignment.Source.Entity,
                                                          parentAssignment.Source.Field), parentTable)
                                            .EqualTo(
                                            Column.Named(childAssignment.Source.Field)
                                                .OwnedBy(childAssignment.Source.Entity)
                                                .AliasOwner(_joinAlias.ContainsKey(childAssignment.Source.Entity) ? _joinAlias[childAssignment.Source.Entity] : childAssignment.Source.Entity)
                                                )
                                        as IClause;
                                }
                    )
                );
        }

        public SelectBuilder(IRepositoryMapIndex maps, Type type, ITargetRelationship relationshipToParent, string suffix, ILoadInstruction instruction)
        {
            _repositoryMapIndex = maps;
            _tableSuffix = relationshipToParent == null ? "" : relationshipToParent.RelativeProperty;
            _parentSuffix = suffix;
            _owningRelationship = relationshipToParent;
            TargetType = type;
            Instruction = instruction;
        }
    }
}